Using DiffUtil and ListAdapter for Efficient Updates
Course: Android App Development
Topic: Using DiffUtil and ListAdapter for Efficient Updates
Course Link: Android App Development Course
Demo Link: Register for a Course Demo
Website: JustAcademy
1. Introduction
RecyclerView is widely used in Android applications to display collections of information, such as products, student records, contacts, messages, and transaction histories. When the underlying data changes, the application must update the displayed list without causing unnecessary work or poor scrolling performance.
DiffUtil and ListAdapter help solve this problem. DiffUtil calculates the differences between an old list and a new list, while ListAdapter provides a convenient adapter implementation that uses DiffUtil to apply those changes efficiently.
Instead of refreshing every row whenever the data changes, these tools identify which items were inserted, removed, moved, or changed. RecyclerView can then apply targeted updates and item animations.
2. Learning Objectives
- Understand why efficient RecyclerView updates are important.
- Learn the purpose and working of DiffUtil.
- Understand the difference between DiffUtil, ListAdapter, and notifyDataSetChanged().
- Create a DiffUtil.ItemCallback implementation.
- Build a custom ListAdapter in Kotlin.
- Submit updated lists using submitList().
- Handle item clicks and list modifications safely.
- Identify common mistakes involving mutable lists, identity, and content comparisons.
3. Why Efficient List Updates Matter
Imagine a shopping application displaying 500 products. A user changes the price of one product or removes a single item. Refreshing the entire list can cause unnecessary rebinding, visual flickering, and extra UI work.
Efficient updates allow RecyclerView to focus on the differences between the previous and current data sets. This can improve responsiveness and make item animations more natural.
Common Problems with Full List Refreshes
- Unnecessary rebinding of unchanged items.
- Potential flickering or loss of some item-specific UI state.
- Extra processing when only a few records have changed.
- Less precise item animations.
- More manual work when managing insertions, deletions, and changes.
4. What Is DiffUtil?
DiffUtil is an AndroidX utility class that calculates the difference between two lists and generates update operations that transform the old list into the new list. It uses an algorithm based on Eugene W. Myers's difference algorithm and can detect moved items in an additional pass.
DiffUtil does not automatically manage every aspect of your application's data. You must provide the old and new lists and define how items and their contents should be compared. You can then apply the calculated changes to a RecyclerView adapter.
Example
Suppose the original product list contains:
Old list:
1. Laptop - ₹50,000
2. Phone - ₹20,000
3. Headphones - ₹2,000
The updated list contains:
New list:
1. Laptop - ₹50,000
2. Phone - ₹18,000
3. Tablet - ₹30,000
DiffUtil can identify that the phone's content changed and the headphones were replaced by a tablet. Depending on item identity and list ordering, it calculates the relevant updates rather than requiring a full refresh.
5. What Is ListAdapter?
ListAdapter is an AndroidX class that extends RecyclerView.Adapter and integrates DiffUtil through an asynchronous list-differ mechanism. It manages the current displayed list and calculates differences when you submit new lists.
ListAdapter is usually the simplest choice when you need to display a list that changes over time. It reduces boilerplate code because you do not have to manually calculate and dispatch each update.
Important ListAdapter Features
- Uses DiffUtil.ItemCallback to compare items.
- Provides
submitList() to supply updated data.
- Provides
getItem(position) to retrieve the item at a position.
- Provides
getItemCount() based on the current list.
- Calculates list differences asynchronously by default.
- Dispatches relevant adapter updates on the main thread.
6. DiffUtil vs. ListAdapter vs. notifyDataSetChanged()
| Feature |
DiffUtil |
ListAdapter |
notifyDataSetChanged() |
| Main purpose |
Calculates differences between lists. |
Manages a displayed list using DiffUtil. |
Notifies RecyclerView that the data may have changed. |
| Boilerplate |
More manual implementation is required. |
Less code for common list updates. |
Simple to call, but does not describe individual changes. |
| Update granularity |
Produces specific update operations. |
Applies calculated item-level updates. |
Requests a broad refresh. |
| Asynchronous calculation |
Manual DiffUtil usage requires you to manage threading when needed. |
Diff calculation is handled asynchronously by default. |
No list diff calculation is performed. |
| Best suited for |
Custom adapter update workflows. |
Most lists that regularly receive new data. |
Simple cases where precise updates are unnecessary. |
7. Understanding DiffUtil.ItemCallback
When using ListAdapter, you usually implement DiffUtil.ItemCallback<T>, where T is the model type displayed by the list. This callback contains two important methods.
7.1 areItemsTheSame()
This method determines whether two objects represent the same logical item. It should normally compare a stable, unique identifier such as a database ID or product ID.
override fun areItemsTheSame(
oldItem: Product,
newItem: Product
): Boolean {
return oldItem.id == newItem.id
}
For example, a product with ID 10 remains the same logical product even if its price changes. Its ID should remain the same across updates.
7.2 areContentsTheSame()
This method determines whether the displayed content of two objects is equivalent. If the model is an immutable Kotlin data class and its equality comparison includes all relevant displayed properties, you can compare the objects directly.
override fun areContentsTheSame(
oldItem: Product,
newItem: Product
): Boolean {
return oldItem == newItem
}
If only certain fields affect the UI, you can compare those fields explicitly. However, every property that affects the displayed content should be considered. Otherwise, a visible change might not trigger a rebind.
Difference Between the Two Methods
| Method |
Question answered |
Example |
areItemsTheSame() |
Are these the same logical record? |
Both products have ID 10. |
areContentsTheSame() |
Would these records display the same content? |
Both products have the same name and price. |
8. Step One: Add the RecyclerView Dependency
In a standard Android Views project, make sure the RecyclerView library is included in the app module. Use a stable version compatible with the project's Android Gradle Plugin and dependency configuration.
dependencies {
implementation("androidx.recyclerview:recyclerview:<current-stable-version>")
}
Replace the placeholder with the appropriate stable version, then synchronize the Gradle project.
9. Step Two: Create a Data Model
In this example, we will build a product list. Each product has a unique ID, a name, and a price.
data class Product(
val id: Int,
val name: String,
val price: Double
)
A Kotlin data class provides value-based equality by default. This makes comparing old and new product objects straightforward when all relevant properties are represented in the class.
10. Step Three: Create the Item Layout
Create a file named item_product.xml in the res/layout directory. It will display one product in RecyclerView.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/txtProductName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/txtProductPrice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="14sp" />
</LinearLayout>
11. Step Four: Create a ViewHolder
The ViewHolder stores references to the views in the item layout.
class ProductViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView) {
private val nameTextView: TextView =
itemView.findViewById(R.id.txtProductName)
private val priceTextView: TextView =
itemView.findViewById(R.id.txtProductPrice)
fun bind(product: Product) {
nameTextView.text = product.name
priceTextView.text =
"₹${"%.2f".format(product.price)}"
}
}
Import android.view.View, android.widget.TextView, and androidx.recyclerview.widget.RecyclerView. In production applications, use a consistent locale-aware currency formatter when displaying prices.
12. Step Five: Implement ProductDiffCallback
Now define how the adapter should compare products.
class ProductDiffCallback :
DiffUtil.ItemCallback<Product>() {
override fun areItemsTheSame(
oldItem: Product,
newItem: Product
): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(
oldItem: Product,
newItem: Product
): Boolean {
return oldItem == newItem
}
}
Import androidx.recyclerview.widget.DiffUtil. The first method compares stable identity; the second checks whether the model's values have changed.
13. Step Six: Build the ListAdapter
The custom adapter extends ListAdapter and supplies the callback to its constructor. The framework manages the list comparison and update dispatching.
class ProductAdapter(
private val onProductClick: (Product) -> Unit
) : ListAdapter<Product, ProductViewHolder>(
ProductDiffCallback()
) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ProductViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_product, parent, false)
return ProductViewHolder(view)
}
override fun onBindViewHolder(
holder: ProductViewHolder,
position: Int
) {
val product = getItem(position)
holder.bind(product)
holder.itemView.setOnClickListener {
onProductClick(product)
}
}
}
Import android.view.LayoutInflater, android.view.ViewGroup, androidx.recyclerview.widget.ListAdapter, and androidx.recyclerview.widget.RecyclerView. ListAdapter already implements getItemCount(), so you do not need to override it for this example.
14. Step Seven: Set Up RecyclerView
Place RecyclerView in your Activity layout and attach a LayoutManager and the custom adapter.
val recyclerView =
findViewById<RecyclerView>(R.id.recyclerView)
val productAdapter = ProductAdapter { product ->
Toast.makeText(
this,
"Selected: ${product.name}",
Toast.LENGTH_SHORT
).show()
}
recyclerView.layoutManager =
LinearLayoutManager(this)
recyclerView.adapter = productAdapter
val initialProducts = listOf(
Product(1, "Laptop", 50000.0),
Product(2, "Phone", 20000.0),
Product(3, "Headphones", 2000.0)
)
productAdapter.submitList(initialProducts)
This code assumes it is executed inside an Activity after the content layout has been loaded. Required imports include androidx.recyclerview.widget.LinearLayoutManager, androidx.recyclerview.widget.RecyclerView, and android.widget.Toast.
15. Updating Data with submitList()
The most important ListAdapter method is submitList(). Call it whenever you have a new snapshot of the data that should be displayed.
val updatedProducts = listOf(
Product(1, "Laptop", 50000.0),
Product(2, "Phone", 18000.0),
Product(4, "Tablet", 30000.0)
)
productAdapter.submitList(updatedProducts)
ListAdapter calculates the difference between the current list and the submitted list. It then dispatches the required adapter updates. The old list may remain visible while the new list is being calculated.
Always submit a new list when the data changes. Do not modify the list currently being displayed by ListAdapter or mutate its model objects in place, because doing so can prevent DiffUtil from detecting changes reliably.
16. Adding, Removing, and Editing Items
16.1 Adding an Item
val currentProducts = productAdapter.currentList
val newProduct = Product(5, "Keyboard", 1500.0)
productAdapter.submitList(
currentProducts + newProduct
)
16.2 Removing an Item
val updatedProducts = productAdapter.currentList
.filterNot { it.id == 3 }
productAdapter.submitList(updatedProducts)
16.3 Editing an Item
val updatedProducts = productAdapter.currentList.map { product ->
if (product.id == 2) {
product.copy(price = 17500.0)
} else {
product
}
}
productAdapter.submitList(updatedProducts)
These examples create new list snapshots and preserve the original data objects. The copy() function creates an updated Product instance while leaving the previous instance unchanged.
17. Using a Commit Callback
submitList() also supports a commit callback that runs when the submitted list has been committed for display. This can be useful for actions that should happen after a particular list update has been applied.
productAdapter.submitList(updatedProducts) {
// The submitted list has been committed.
// Perform any follow-up UI action here.
}
A callback for a superseded submission may not run if a newer list replaces it before it is committed. Do not rely on every submission callback executing in rapid update scenarios.
18. Using ListAdapter with ViewModel
In a larger Android application, a ViewModel can own the screen's UI data. The Activity or Fragment observes the state and submits the latest list to the adapter.
class ProductViewModel : ViewModel() {
private val _products =
MutableLiveData<List<Product>>(emptyList())
val products: LiveData<List<Product>> =
_products
fun updateProducts(newProducts: List<Product>) {
_products.value = newProducts.toList()
}
}
Observe the list from an Activity or Fragment using its lifecycle owner:
viewModel.products.observe(this) { products ->
productAdapter.submitList(products)
}
This example assumes the adapter has already been initialized. In a Fragment, use viewLifecycleOwner as the lifecycle owner for view-related observation. Ensure updates to UI state occur on the appropriate thread.
19. Using DiffUtil Directly
Although ListAdapter is generally simpler, you can use DiffUtil directly when you need to retain a custom RecyclerView.Adapter implementation or manage update operations yourself.
A custom DiffUtil.Callback compares the old and new lists using their sizes, item identities, and content values.
class ProductListDiffCallback(
private val oldList: List<Product>,
private val newList: List<Product>
) : DiffUtil.Callback() {
override fun getOldListSize(): Int = oldList.size
override fun getNewListSize(): Int = newList.size
override fun areItemsTheSame(
oldItemPosition: Int,
newItemPosition: Int
): Boolean {
return oldList[oldItemPosition].id ==
newList[newItemPosition].id
}
override fun areContentsTheSame(
oldItemPosition: Int,
newItemPosition: Int
): Boolean {
return oldList[oldItemPosition] ==
newList[newItemPosition]
}
}
With a custom adapter, calculate the diff between immutable snapshots, replace the adapter's backing data with the new list, and immediately dispatch the resulting updates to the adapter. For large lists, perform the calculation off the main thread and dispatch updates on the main thread. Avoid this manual approach when ListAdapter already meets your needs.
20. How DiffUtil Handles Identity and Content
Correct comparison logic is critical. Consider a product with ID 7 whose name changes from "Basic Phone" to "Smart Phone".
- Same ID: The two records represent the same product, so
areItemsTheSame() returns true.
- Different name: The content changed, so
areContentsTheSame() returns false.
- Result: DiffUtil can identify the existing item as changed rather than treating it as an unrelated new record.
If the ID changes for every refresh, DiffUtil may treat the records as removals and insertions. Use stable identifiers that represent the real identity of each item.
21. Common Mistakes and Troubleshooting
| Problem |
Cause |
Solution |
| Updated text does not appear |
areContentsTheSame() returns true despite a visible change. |
Compare all properties that affect the UI. |
| Rows are recreated unnecessarily |
areItemsTheSame() uses an unstable identifier. |
Compare a stable, unique ID. |
| Changes are not detected |
The submitted list or its objects were mutated in place. |
Use immutable model objects and submit a new list snapshot. |
| Everything refreshes after every update |
The app calls notifyDataSetChanged() or comparison logic marks all items as changed. |
Use ListAdapter and correct DiffUtil comparisons. |
| Incorrect row responds to a click |
A stale adapter position was captured. |
Capture the bound model object or validate the current binding adapter position when necessary. |
| Commit callback does not run |
A newer submitted list superseded the pending submission. |
Do not assume every submission callback will execute. |
| List updates cause slow UI |
Large calculations or expensive work run on the main thread. |
Use ListAdapter's asynchronous diffing and keep binding lightweight. |
22. Best Practices
- Prefer ListAdapter for most RecyclerView lists that receive updates.
- Use stable, unique model identifiers in
areItemsTheSame().
- Compare every relevant displayed property in
areContentsTheSame().
- Prefer immutable data classes and new list snapshots.
- Use
submitList() instead of manually notifying each update when using ListAdapter.
- Do not modify the list returned by
currentList; treat it as read-only.
- Keep database operations, network requests, and expensive calculations out of
onBindViewHolder().
- Use a ViewModel or repository to separate data management from UI rendering.
- Test insertions, deletions, content changes, reordering, empty lists, and rapid consecutive updates.
- Use direct DiffUtil callbacks only when a custom adapter workflow requires them.
23. Practical Exercise
Project: Build a Product Catalog with Efficient Updates.
- Create a Product model with ID, name, price, and category.
- Create an XML layout for a product row.
- Implement a ProductViewHolder and ProductAdapter using ListAdapter.
- Create a ProductDiffCallback to compare product identity and content.
- Display sample products using RecyclerView and LinearLayoutManager.
- Add an option to update a product's price.
- Add a delete action to remove a product.
- Add a new product and verify that only the required list changes are applied.
- Test what happens when two lists are submitted in quick succession.
Expected outcome: The catalog displays the correct products and updates smoothly when items are added, edited, removed, or reordered.
24. Interview Questions and Answers
Q1. What is DiffUtil?
DiffUtil calculates the differences between two lists and provides update operations that allow RecyclerView to reflect those changes efficiently.
Q2. What is ListAdapter?
ListAdapter is a RecyclerView adapter base class that integrates DiffUtil and simplifies displaying updated lists.
Q3. What is the purpose of areItemsTheSame()?
It determines whether an old item and a new item represent the same logical entity, typically by comparing unique IDs.
Q4. What is the purpose of areContentsTheSame()?
It determines whether the contents relevant to the UI are unchanged.
Q5. What does submitList() do?
It submits a new list to ListAdapter, which calculates and applies the necessary changes to the displayed data.
Q6. Why should lists not be mutated after submission?
DiffUtil compares old and new snapshots. Mutating a submitted list or its relevant objects can change the old state and prevent accurate difference detection.
Q7. Does ListAdapter calculate differences on the main thread?
ListAdapter uses asynchronous diff calculation by default and dispatches the resulting adapter updates on the main thread.
Q8. When should notifyDataSetChanged() be avoided?
When a more precise update mechanism is available, because a full refresh does not describe which individual items changed.
Q9. What is the difference between DiffUtil.Callback and DiffUtil.ItemCallback?
DiffUtil.Callback supports manually comparing two lists by position, while DiffUtil.ItemCallback is designed for comparing individual items and is commonly used with ListAdapter.
Q10. Can DiffUtil detect moved items?
Yes. DiffUtil performs an additional pass to detect item moves when move detection is enabled.
25. Quick Revision
- DiffUtil: Calculates differences between old and new lists.
- ListAdapter: Integrates DiffUtil into a RecyclerView adapter.
- Item identity: Compare stable IDs using
areItemsTheSame().
- Content equality: Compare UI-relevant values using
areContentsTheSame().
- submitList(): Submit a new list snapshot.
- currentList: Read the adapter's current list; do not mutate it.
- Immutability: Keep submitted lists and relevant item values unchanged.
- Best default: Use ListAdapter unless a custom update workflow is needed.
26. Official Learning Resources
Conclusion
DiffUtil and ListAdapter help Android developers update RecyclerView lists without unnecessarily refreshing every item. DiffUtil calculates the changes, while ListAdapter manages list submission and applies targeted updates. By using stable item IDs, accurate content comparisons, immutable data, and submitList(), developers can create responsive and maintainable interfaces for dynamic data.
Learn more through the JustAcademy Android App Development course, register for a course demo, or visit the JustAcademy homepage.